using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using DuinoWebserver.Core.Helpers;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace DuinoWebserver
{
    public delegate void RequestDelegate(Request request);

    public struct PathHandler
    {
        public string Path { get; set; }
        public HttpRequestMethod Method { get; set; }
        public RequestDelegate Handler { get; set; }
    }

    public class WebServer : IDisposable
    {
        public string WebFolder { get; set; }

        private const int maxRequestSize = 256;
        public readonly int portNumber = 80;
        private Socket listeningSocket = null;
        private ArrayList pathHandlers = new ArrayList();
        private Thread _workerThread;

        public WebServer() : this(80){}
        public WebServer(int PortNumber)
        {
            portNumber = PortNumber;
            listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            listeningSocket.Bind(new IPEndPoint(IPAddress.Any, portNumber));
            listeningSocket.Listen(10);

            _workerThread = new Thread(StartListening);
            _workerThread.Start();
        }

        public bool Stop()
        {
            if (_workerThread == null)
                return false;

            if (listeningSocket != null)
                listeningSocket.Close();

            _workerThread.Abort();
            return true;
        }

        ~WebServer()
        {
            Dispose();
        }

        public void AddPathHandler(string path, HttpRequestMethod method, RequestDelegate handler)
        {
            var pathHandler = new PathHandler { Path = path.ToLower(), Method = method, Handler = handler };
            pathHandlers.Add(pathHandler);
        }

        private void onRequestReceived(Request request)
        {
            foreach (PathHandler pathHandler in pathHandlers)
            {
                // method match
                if (pathHandler.Method != request.Method
                    && pathHandler.Method != HttpRequestMethod.Any)
                    continue;

                // exact path match
                var path = pathHandler.Path;
                var url = request.Url.ToLower();
                var match = path == url;

                // path pattern match 
                if (!match)
                {
                    var pathLength = path.Length;
                    if (path[pathLength - 1] != '*')
                        continue;

                    path = path.Substring(0, pathLength - 1);
                    match = url.IndexOf(path) == 0;
                    if (!match)
                        continue;
                }
                pathHandler.Handler(request);
                break;
            }
        }

        public void StartListening()
        {
            while (true)
            {
                try
                {
                    using (Socket clientSocket = listeningSocket.Accept())
                    {
                        clientSocket.ReceiveTimeout = 200;
                        clientSocket.SendTimeout = 1000;

                        respondRequest(clientSocket);
                    }
                }
                catch (SocketException e)
                {
                    Debug.Print(e.Message + " Error Code: " + e.ErrorCode.ToString());
                }
                catch (Exception e)
                {
                    Debug.Print(e.Message);
                }
                // This helps prevent lock-ups in a continuous loop. 
                Thread.Sleep(10);
            }
        }

        private void _GC()
        {
            var size = Microsoft.SPOT.Debug.GC(true);
            Debug.Print("GC free size: " + size.ToString());
        }
        
        private void respondRequest(Socket clientSocket)
        {
            IPEndPoint clientIP = clientSocket.RemoteEndPoint as IPEndPoint;
            Debug.Print("Received request from " + clientIP.ToString());

            _GC();

            bool complete = false;
            byte[] headerBuffer = new byte[0];

            while (!complete)
            {
                var availableBytes = WaitForSocketAvailable(clientSocket);
                if (availableBytes < 0)
                {
                    Debug.Print("*** Socket unavailable.");
                    return;
                }

//                Debug.Print(DateTime.Now.ToString() + " " + availableBytes.ToString() + " request bytes available");

                int bytesReceived = (availableBytes > maxRequestSize ? maxRequestSize : availableBytes);
                if (bytesReceived > 0)
                {
                    var buffer = getRequestHeader(clientSocket, bytesReceived, out complete);
                    headerBuffer = Utility.CombineArrays(headerBuffer, buffer);
                }
            }

            var headerData = Encoding.UTF8.GetChars(headerBuffer);
            using (Request request = new Request(clientSocket, headerData))
            {
                Debug.Print(DateTime.Now.ToString() 
                    + " Request from " + clientIP.ToString() + " "
                    + Request.MethodToString(request.Method) + " " 
                    + request.Url + ".");
                onRequestReceived(request);
            }
        }

        public static int WaitForSocketAvailable(Socket clientSocket)
        {
            return WaitForSocketAvailable(clientSocket, 500);
        }

        public static int WaitForSocketAvailable(Socket clientSocket, int delay)
        {
            // for delayed data receive
            var dataReady = clientSocket.Poll(-1, SelectMode.SelectRead);
            var available = clientSocket.Available;
            if (dataReady && available > 0)
                return available;
            return -1;

//            while (delay > 0)
//            {
//                int available = clientSocket.Available;
//                if (available > 0) return available;

//                Thread.Sleep(1);
////                Debug.Print("WaitForSocketAvailable: " + delay.ToString());
//                delay--;
//            }
//            throw new Exception("Timeout while wait for socket avilable (Listener.WaitForSocketAvailable).");
        }

        private enum HeaderState
        {
            None,
            FirstCR,
            FirstNL,
            SecondCR,
            SecondNL
        }

        private byte[] getRequestHeader(Socket clientSocket, int bytesReceived, out bool complete)
        {
            complete = false;
            HeaderState state = HeaderState.None;
            var received = 0;

            byte[] buffer = new byte[bytesReceived];
            byte[] charBuffer = new byte[1];
            while (clientSocket.Receive(charBuffer, 1, SocketFlags.None) == 1)
            {
                var ch = charBuffer[0];
                buffer[received++] = ch;

                switch (state)
                {
                    case HeaderState.None:
                        if (ch == 13) state = HeaderState.FirstCR;
                        break;
                    case HeaderState.FirstCR:
                        state = (ch == 10) ? HeaderState.FirstNL : HeaderState.None;
                        break;
                    case HeaderState.FirstNL:
                        state = (ch == 13) ? HeaderState.SecondCR : HeaderState.None;
                        break;
                    case HeaderState.SecondCR:
                        state = (ch == 10) ? HeaderState.SecondNL : HeaderState.None;
                        break;
                    case HeaderState.SecondNL:
                        break;
                    default:
                        break;
                }

                if (state == HeaderState.SecondNL)
                {
                    complete = true;
                    break;
                }

                if (received >= bytesReceived)
                    break;
            }

            byte[] result = Utility.ExtractRangeFromArray(buffer, 0, received);
            return result;
        }

        #region Default Handler

        public void AddDefaultPathHandlers()
        {
            AddPathHandler("/*", HttpRequestMethod.Get, DefaultGetHandler);
            AddPathHandler("/*", HttpRequestMethod.Put, DefaultPutHandler);
            AddPathHandler("/*", HttpRequestMethod.Post, DefaultPostHandler);
            AddPathHandler("/*", HttpRequestMethod.Delete, DefaultDeleteHandler);
            AddPathHandler("/*", HttpRequestMethod.Options, DefaultOptionsHandler);
            AddPathHandler("/*", HttpRequestMethod.Move, DefaultMoveHandler);
        }

        private void DefaultOptionsHandler(Request request)
        {
            request.SendOptionsResponse();
        }

        public virtual void DefaultPutHandler(Request request)
        {
            // Receive a file
            var filePath = WebFolder + request.Url.Replace('/', '\\');
            var directory = Path.GetDirectoryName(filePath);

            if (File.Exists(filePath))
                File.Delete(filePath);
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            request.StoreFile(filePath);

            sendFileOperationResponse(request, filePath, "created");
        }

        public virtual void DefaultDeleteHandler(Request request)
        {
            string filePath = WebFolder + request.Url.Replace('/', '\\');

            if (Directory.Exists(filePath))
            {
                Directory.Delete(filePath);
            }
            else if (!File.Exists(filePath))
            {
                request.SendNotFoundResponse();
                return;
            }
            File.Delete(filePath);
            sendFileOperationResponse(request, filePath, "deleted");
        }

        /// <summary>
        /// Look for a file on the SD card and send it back if it exists
        /// Look for a directory on the SD card and send index as html
        /// Send 404 when file/directory not found.
        /// </summary>
        /// <param name="request"></param>
        public virtual void DefaultGetHandler(Request request)
        {
            // Replace slash with backslash
            string filePath = WebFolder + request.Url.Replace('/', '\\');

            // strip ? extension
            var queryStart = filePath.IndexOf('?');
            if (queryStart >= 0)
                filePath = filePath.Substring(0, queryStart);

            if (Directory.Exists(filePath))
                serveDirectoryContent(filePath, request);
            else if (File.Exists(filePath))
                request.SendFile(filePath);
            else
                request.SendNotFoundResponse();
        }

        public virtual void DefaultPostHandler(Request request)
        {
            request.SendNotFoundResponse();
        }

        public virtual void DefaultMoveHandler(Request request)
        {
            string filePath = request.Url.Replace('/', '\\');
            string dir = filePath.Substring(0, filePath.LastIndexOf('\\') + 1);
            filePath = WebFolder + filePath;
            int res = WaitForSocketAvailable(request.ClientSocket);
            if (res > 0)
            {
                var buf = new byte[res];
                int rec = request.ClientSocket.Receive(buf);

                var dest = new string(Encoding.UTF8.GetChars(buf));
                Debug.Print("Move: " + rec + " of " + res + " " + dest);

                bool success = false;
                try
                {
                    if (File.Exists(filePath))
                    {
                        File.Move(filePath, WebFolder + '\\' + dir + '\\' + dest);
                        success = true;
                    }
                    else if (Directory.Exists(filePath))
                    {
                        Directory.Move(filePath, WebFolder + '\\' + dir + '\\' + dest);
                        success = true;
                    }
                }
                catch (Exception e)
                {
                    // catch all exceptions
                    Debug.Print(e.ToString());
                }
                string msg = success ? "moved to " + dest : "failed: move";
                sendFileOperationResponse(request, filePath, msg);
            }
            else request.SendResponse("failed: move");
        }

        private void serveDirectoryContent(string pTargetUri, Request request)
        {
            var baseDir = new DirectoryInfo(pTargetUri);
            var renderPath = request.Url.TrimEnd('/');
            var responseBuilder = request;

            responseBuilder.SendHeader(0);
            responseBuilder.Append("<html><head><title>");
            responseBuilder.Append(renderPath);
            responseBuilder.Append("/</title></head><body><h1>");
            responseBuilder.Append(renderPath);
            responseBuilder.Append("/</h1><hr><pre>");
            if (renderPath != String.Empty)
            {
                var parentSeparatorIndex = renderPath.LastIndexOf('/');
                var parentUri = "";
                if (parentSeparatorIndex > 0)
                {
                    parentUri = renderPath.Substring(0, parentSeparatorIndex);
                }
                responseBuilder.Append("<a href=\"");
                responseBuilder.Append(urlEncode(parentUri));
                responseBuilder.Append("/\">[To Parent Directory]</a><br>");
            }

            _GC();
            Debug.Print("GetDirectories");
            foreach (DirectoryInfo dir in baseDir.GetDirectories())
            {
                buildDirectoryContentItem(responseBuilder, pTargetUri, renderPath, dir, true);
            }
            _GC();
            Debug.Print("GetFiles");
            foreach (FileInfo file in baseDir.GetFiles())
            {
                buildDirectoryContentItem(responseBuilder, pTargetUri, renderPath, file, false);
            }
            responseBuilder.Append("</pre><hr></body></html>");
            responseBuilder.Flush();
        }

        private void buildDirectoryContentItem(Request pResponseBuilder, string pTargetUri, string pRequestUrl, FileSystemInfo pFileInfo, bool pIsDirectory)
        {
            pResponseBuilder.Append(
                pFileInfo.LastWriteTime.ToString("yyyy-MM-dd hh:mm  ")
                + (pIsDirectory ?
                   "&lt;dir&gt;  ".PadRight(19) :
                    ((FileInfo)pFileInfo).Length.ToString().PadRight(13))
                + " <a href=\""
                + urlEncode(pRequestUrl + "/" + pFileInfo.Name)
                + "\">" + pFileInfo.Name + "</a><br>");
            return;
        }

        private string urlEncode(string value)
        {
            //Uri.EscapeUriString don't exists in NETMF
            var escChar = " \n\r\t[]{}%<>|\\^";//#?/$&=+:;,";
            if (!NeedsEscaping(value, escChar)) return value;

            var dat = System.Text.Encoding.UTF8.GetBytes(value);
            var sb = new System.Text.StringBuilder(dat.Length);

            for (int i = 0; i < dat.Length; i++)
            {
                if (dat[i] < 127 && escChar.IndexOf((char)dat[i]) < 0) sb.Append((char)dat[i]);
                else
                {
                    sb.Append('%');
                    var tmp = dat[i].ToString("X");
                    if (tmp.Length < 2) sb.Append("0" + tmp);
                    else sb.Append(tmp);
                }
            }
            return sb.ToString();
        }
        bool NeedsEscaping(string s, string escChar)
        {
            for (int i = 0; i < s.Length; i++)
            {
                if (s[i] > 126) return true;
            }
            return s.IndexOfAny(escChar.ToCharArray()) >= 0;
        }

        private void sendFileOperationResponse(Request request, string filePath, string operationKind)
        {
            request.SendResponse("<html><body><p>File: " + filePath
                + " " + operationKind + " at " + DateTime.Now.ToString()
                + "</p><p>Method: " + request.Method.ToString()
                + "<br />URL: " + request.Url +
                "</p></body></html>");
        }

        #endregion



        #region IDisposable Members

        public void Dispose()
        {
            if (listeningSocket != null) listeningSocket.Close();

        }

        #endregion
    }
}
